一个好的HTTP缓存策略可以显著的提高web应用的表现和客户的体验。Cache-Control
HTTP相应头主要就是负责这个,以及Last-Modified
和ETag
等条件头。
Cache-Control
HTTP响应头建议私有缓存(比如,浏览器)和公有缓存(比如,代理)如何为了重用而缓存HTTP响应。
ETag(entity tag)是一个HTTP响应头,由兼容HTTP/1.1的web服务器返回,用来检测给定的URL是否发生改变。它可以被看作时更加复杂的Last-Modified
头文件。当一个服务器返回了含有ETag头的响应,客户端会在之后的GETs请求中,在If-None-Match
使用这个头。如果这个内容没有被改变,服务器会返回304: Not Modified
。
这节描述了在Spring Web MVC应用中配置HTTP缓存的不同方式。
Spring Web MVC应用支持多种案例和多种方法配置"Cache-Control"头。虽然在RFC 7234的5.2.2节中已经完整的描述了头和它可能的指定,这里还是列出一些符合常用情况的方法。
Spring Web WVC可以通过它的API来方便的配置:setCachePeriod(int seconds)
:
-1
值不会生成Cache-Control
响应头。
0
值将会使用Cache-Control:on-store
指定阻止缓存。
* n > 0
值会使用Cache-Control:max-age=n
将给定的响应缓存n
秒。
CacheControl
构建器类简单的描述了可用的"Cache-Control"指令并让你更容易的构建自己的缓存策略。一旦创建完成,CacheControl
实例可以被多个Spring Web MVC API接受作为参数。
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
.noTransform().cachePublic();
应该要为静态资源选择合适的Cache-Control
和条件头来获得最佳的性能。如果正确配置了ResourceHttpRequestHandler
提供静态资源,不仅可以通过读文件元数据中的Last-Modified
头,还可以使用Cache-Control
头。
你可以设置ResourceHttpRequestHandler
的cachePeriod
属性或是使用CacheControl
实例,它们都支持更具体的指令:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public-resources/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
}
}
使用XML配置时:
<mvc:resources mapping="/resources/**" location="/public-resources/">
<mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>
控制器可以支持Cache-Control
,ETag
,和If-Modified-Since
的HTTP请求;事实上如果想在响应中设置Cache-Control
头的话,也建议这么做。它会计算给定请求的lastModified的long
值或是ETag值,将它和请求的If-Modified-Since
头的值比较,并可能返回一个状态码为304(Not Modified)的响应。
正如在"Using HttpEntity"这节中描述的,控制器可以通过HttpEntity
类型跟请求/响应交互。控制器可以像这样返回的ResponseEntity
包含响应的HTTP缓存信息:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
这样做不仅会让响应包含'ETag'
和Cache-Control
头,当客户端发送的条件头符合控制器设置的缓存信息,它也会将响应的转变成HTTP 304 Not Modified
的响应且响应体为空。
@RequestMapping
方法可能也希望有相同的行为。看下面如何实现:
@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {
long lastModified = // 1. application-specific calculation
if (request.checkNotModified(lastModified)) {
// 2. shortcut exit - no further processing necessary
return null;
}
// 3. or otherwise further request processing, actually preparing content
model.addAttribute(...);
return "myViewName";
}
这里有两个关键的元素:调用request.checkNotModified(lastModified)
和返回null
。前者会在它返回true
时设置响应的装填和头。后者,结合了前者,会使得Spring MVC不会在继续处理请求。
它有三种变体:
request.checkNotModified(lastModified)
会将lastModified和请求头中的If-Modified-Since
或是If-Unmodified-Since
比较
request.checkNotModified(eTag)
会将eTag和请求头中的If-None-Match
比较
* request.checkNotModified(eTag, lastModified)
会将两者都进行比较,必须两个条件都要符合
但收到带条件的GET
/HEAD
请求,checkNotModified
会检查资源是否没被修改过,如果是,它会发出HTTP 304 Not Modified
的响应。至于POST
/PUT
/DELETE
请求,checkNotMidified
会检查资源是否没被修改过,如果是,会生成HTTP 409 Precondition Failed
响应,避免并发的修改。
对ETags的支持是有Servlet过滤器的ShallowEtagHeaderFilter
提供。它是一个普通的Servlet过滤器,因此可以和任何web狂气结合使用。ShallowEtagHeaderFilter
过滤器创造了所谓的浅ETags(和深ETags相反,之后会介绍)。过滤器缓存了被渲染的JSP(或是其他内容),并在其上生成了MD5码,并将它作为响应的ETag头返回。当下次客户端请求相同的资源时,会用它的哈希作为If-None-Match
值。过滤器会检测这点,再次渲染视图,并比较两个哈希值,如果它们相同,则会返回304
。
注意,这个策略只会节省贷款而不会节省CPU,因为响应都必须在每个请求计算完成后。另一种策略是控制器级别的(上面所讨论的),可以节省贷款,并避免计算。
这个过滤器有个writeWeakTag
属性用来配置过滤器去写Weak ETags,像RFC 7323 2.3节中定义的那样:W/"02a2d595e6ed9a0b24f027f2b63b134d6"
。
你可以这样在web.xml中配置ShallowEtagHeaderFilter
:
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
<!-- Optional parameter that configures the filter to write weak ETags
<init-param>
<param-name>writeWeakETag</param-name>
<param-value>true</param-value>
</init-param>
-->
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
或是在Servlet 3.0+的环境中:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new ShallowEtagHeaderFilter() };
}
}